Jerry's Log

Value in Java

contents

자바 생태계에서 @Value 애노테이션에 대해 질문하면 종종 혼란이 발생하곤 합니다. 왜냐하면 완전히 다르지만 둘 다 엄청나게 유명한 두 개의 @Value 애노테이션이 존재하기 때문입니다.

  1. Spring의 @Value (org.springframework.beans.factory.annotation.Value): 애플리케이션에 설정 속성(Property)을 주입할 때 사용합니다.
  2. Lombok의 @Value (lombok.Value): 완벽하게 불변인(Immutable) 데이터 객체를 만들 때 사용합니다.

백엔드 엔지니어라면 이 두 가지를 모두 완벽하게 이해하고 있어야 합니다. 각각에 대한 상세 분석은 다음과 같습니다.


1부: Spring의 @Value (설정값 주입기)

application.yml이나 application.properties 파일에 정의된 변수들을 자바 코드로 가져와야 할 때 사용하는 도구가 바로 Spring의 @Value입니다.

A. 기본 주입 (${...})

가장 흔한 사용법입니다. $ 기호를 사용하여 스프링에게 특정 프로퍼티 이름을 찾아서 값을 넣으라고 지시합니다.

application.yml:

app:
  api-key: "12345-abcde"
  timeout: 5000

자바 코드:

@Service
public class PaymentService {
    
    @Value("${app.api-key}")
    private String apiKey;

    @Value("${app.timeout}")
    private int timeoutMs;
}

B. 기본값 설정 (: 콜론)

만약 .yml 파일에 해당 프로퍼티가 누락되어 있다면 어떻게 될까요? 애플리케이션은 시작되자마자 에러를 뿜으며 죽어버립니다(Crash)! 이를 방지하기 위해 콜론 :을 사용하여 기본값을 지정할 수 있습니다.

// 'app.max-retries' 값이 없으면 기본값인 3이 들어갑니다.
@Value("${app.max-retries:3}")
private int maxRetries;

// 'app.greeting' 값이 없으면 빈 문자열("")이 들어갑니다.
@Value("${app.greeting:}")
private String greeting;

C. SpEL (스프링 표현식) (#{...})

단순히 파일에서 값을 읽어오는 것을 넘어, # 기호를 사용하면 런타임에 자바 코드를 실행하거나 OS 환경 변수를 직접 읽어올 수 있습니다.

// 수학 연산 수행
@Value("#{10 * 5}") // 50이 주입됩니다.
private int calculatedValue;

// OS 시스템 환경 변수를 직접 읽어옴
@Value("#{systemEnvironment['USER']}") 
private String systemUser;

// 앱 시작 시 랜덤 UUID를 생성해서 주입
@Value("#{T(java.util.UUID).randomUUID().toString()}")
private String randomId;

D. 리스트(List)와 배열(Array) 주입

YAML 파일에 콤마(,)로 구분된 문자열을 적어두면, 스프링이 알아서 쪼개어 자바의 ListArray로 만들어 주입해 줍니다.

application.yml:

app:
  supported-countries: US,KR,JP,UK

자바 코드:

// 스프링이 자동으로 콤마를 기준으로 문자열을 잘라 리스트에 담아줍니다!
@Value("${app.supported-countries}")
private List countries; 

E. 주의사항 (흔히 하는 실수들)

  1. 정적(Static) 필드에는 작동 불가: @Valuestatic 필드에 값을 주입할 수 없습니다. 스프링은 클래스가 아닌 생성된 인스턴스(객체) 에 값을 주입하기 때문입니다.
  2. 스프링이 관리하지 않는 객체 (Unmanaged Objects): @Value는 스프링 빈(@Component, @Service, @RestController 등) 내부에서만 동작합니다. 만약 new MyClass() 처럼 직접 객체를 생성했다면, 그 안의 @Value는 전부 무시되고 null 상태로 남습니다.
  3. Final 필드 주입 불가: final 필드에는 필드 주입(Field Injection) 방식으로 값을 넣을 수 없습니다. 반드시 생성자 주입(Constructor Injection) 을 사용해야 합니다. (사실 이 방식이 가장 권장되는 베스트 프랙티스입니다).

베스트 프랙티스 (생성자 주입):

@Service
public class PaymentService {
    private final String apiKey;

    // 생성자를 통해 주입받아 필드를 'final'로 유지하고 스레드 안전성을 확보합니다.
    public PaymentService(@Value("${app.api-key}") String apiKey) {
        this.apiKey = apiKey;
    }
}

2부: Lombok의 @Value (불변 객체 생성기)

도메인 주도 설계(DDD)와 모던 자바 생태계에서는 불변 객체(Immutable Objects, 한 번 생성되면 내부 상태가 절대 변하지 않는 객체) 를 매우 선호합니다. 불변 객체는 태생적으로 스레드에 안전(Thread-safe)하며, 예기치 않은 부수 효과(Side-effect)로 인한 버그를 원천 차단합니다.

Lombok의 @Value는 우리가 흔히 쓰는 @Data의 아주 엄격하고 불변성을 띠는 친척이라고 볼 수 있습니다.

내부 동작 원리

클래스 위에 @Value를 붙이면, 롬복은 컴파일 타임에 다음과 같은 일들을 처리합니다:

  1. 클래스를 final로 만듭니다. (더 이상 상속 불가)
  2. 모든 필드를 private final로 만듭니다. (반드시 생성자에서 값이 세팅되어야 함)
  3. 모든 필드를 인자로 받는 생성자(@AllArgsConstructor)를 만듭니다.
  4. 모든 필드에 대한 Getter 메서드를 만듭니다.
  5. equals(), hashCode(), toString() 메서드를 만듭니다.
  6. 핵심: Setter 메서드는 절대 만들지 않습니다.

사용 예시

우리가 작성하는 코드:

import lombok.Value;

@Value
public class Money {
    String currency;
    long amount;
}

자바가 실제로 컴파일하는 코드:

public final class Money {
    private final String currency;
    private final long amount;

    public Money(String currency, long amount) {
        this.currency = currency;
        this.amount = amount;
    }

    public String getCurrency() { return this.currency; }
    public long getAmount() { return this.amount; }
    
    // ... equals, hashCode, toString 역시 생성됨
}

이제 어떤 개발자가 money.setAmount(100); 같은 코드를 작성하려고 하면 컴파일 에러가 발생합니다. 금액(amount)을 바꾸려면 아예 새로운 Money 객체를 새로 생성하는 방법밖에 없습니다.


요약 비교 테이블

특징 Spring의 @Value Lombok의 @Value
패키지 경로 org.springframework.beans.factory... lombok.Value
적용 대상 필드(Fields), 메서드 파라미터 클래스(Classes)
목적 외부 설정값(YAML/환경변수 등)을 앱에 주입 불변(Immutable) 값 객체(DTO, VO) 생성
클래스 변형 여부 아니오 예 (클래스/필드를 final로 만들고 메서드 자동 생성)
전제 조건 해당 클래스가 반드시 스프링 빈(Bean)이어야 함 롬복(Lombok) 라이브러리가 설치되어 있어야 함

references